#include <assert.h>
#include <stdlib.h>

#include <common/macro/build.h>
#include <common/filelock.h>
#include <common/io.h>
#include <common/util.h>
#include <common/macro/debug.h>
#include <transaction/transaction.h>
#include <transaction/transactionlog.h>

API_LIBLOCAL transaction_log_t *transaction_log_open(const char *filename) {
	size_t i;
	transaction_log_t *transaction_log;
	transaction_id_t tmp;

	transaction_log = malloc(sizeof(*transaction_log));
	
	transaction_log->maxitems = TRANSACTION_MAX_CONCURRENT; /* TODO FIXME, hardcode it somewhere nice. */
	transaction_log->file = io_open_rwc(filename);
	if (transaction_log->file == NULL) {
		transaction_log_close(transaction_log);
		return NULL;
	}

	/* Populate the IDs, if necessary. */
	transaction_log_writelock_current(transaction_log);
	transaction_log_writelock_vacuum(transaction_log);
	if (transaction_log_get_current(transaction_log, &tmp) != 0) {
		tmp = 0;
		if (transaction_log_set_current(transaction_log, tmp) != 0) {
			transaction_log_close(transaction_log);
			return NULL;
		}
		if (transaction_log_get_vacuum(transaction_log, &tmp) != 0) {
			tmp = 0;
			if (transaction_log_set_vacuum(transaction_log, tmp) != 0) {
				transaction_log_close(transaction_log);
				return NULL;
			}
		}

		/*
		 * Populate the transactions, as COMMITED. We do not mind thinking
		 * that transactions that haven't happened yet were succesful.
		 *
		 * TODO: this also simplifies log dumps, because they are of fixed size.
		 */
		for (i = 0; i < transaction_log->maxitems; i++) {
			transaction_log_write_phase(transaction_log, i, COMMITED);
		}
	}
	transaction_log_unlock_current(transaction_log);
	transaction_log_unlock_vacuum(transaction_log);

	return transaction_log;
}

API_LIBLOCAL inline transaction_id_t transaction_log_replaces(transaction_log_t *transaction_log, transaction_id_t transaction_id) {
	return transaction_id - transaction_log->maxitems;
}

API_LIBLOCAL void transaction_log_close(transaction_log_t *transaction_log) {
	assert(transaction_log != NULL);

	if (transaction_log->file != NULL)
		io_close(transaction_log->file);
	free(transaction_log);
}

API_FILELOCAL inline off_t transaction_log_get_offset(transaction_log_t *transaction_log, transaction_id_t transaction_id) {
	assert(transaction_log != NULL);

	return (2 * sizeof(transaction_id_t) + 1 * (transaction_id % transaction_log->maxitems));
}

API_LIBLOCAL int transaction_log_get_current(transaction_log_t *transaction_log, transaction_id_t *transaction_id) {
	assert(transaction_log != NULL);
	assert(transaction_id != NULL);

	io_seek(transaction_log->file, 0, SEEK_SET);
	return (io_read(transaction_id, sizeof(*transaction_id), 1, transaction_log->file) == 1 ? 0 : -1);
}

API_LIBLOCAL int transaction_log_set_current(transaction_log_t *transaction_log, transaction_id_t transaction_id) {
	assert(transaction_log != NULL);

	io_seek(transaction_log->file, 0, SEEK_SET);
	return (io_write(&transaction_id, sizeof(transaction_id), 1, transaction_log->file) == 1 ? 0 : -1);
}

API_LIBLOCAL int transaction_log_get_vacuum(transaction_log_t *transaction_log, transaction_id_t *transaction_id) {
	assert(transaction_log != NULL);
	assert(transaction_id != NULL);

	io_seek(transaction_log->file, sizeof(*transaction_id), SEEK_SET);
	return (io_read(transaction_id, sizeof(*transaction_id), 1, transaction_log->file) == 1 ? 0 : -1);
}

API_LIBLOCAL int transaction_log_set_vacuum(transaction_log_t *transaction_log, transaction_id_t transaction_id) {
	assert(transaction_log != NULL);

	io_seek(transaction_log->file, sizeof(transaction_id), SEEK_SET);
	return (io_write(&transaction_id, sizeof(transaction_id), 1, transaction_log->file) == 1 ? 0 : -1);
}

API_LIBLOCAL int transaction_log_flush(transaction_log_t *transaction_log) {
	return io_sync(transaction_log->file);
}

API_FILELOCAL transaction_phase_e transaction_log_char_to_phase(const char phase_encoded) {
	switch (phase_encoded) {
		case 'A':
			return ACTIVE;
	
		case 'P':
			return PARTIALLY_COMMITED;
	
		case 'C':
			return COMMITED;
	
		case 'F':
			return FAILED;
	
		case 'X':
			return ABORTED;

		default:
			return UNKNOWN;
	}
}

API_FILELOCAL char transaction_log_phase_to_char(const transaction_phase_e phase) {
	switch (phase) {
		case ACTIVE:
			return 'A';

		case PARTIALLY_COMMITED:
			return 'P';

		case COMMITED:
			return 'C';

		case FAILED:
			return 'F';

		case ABORTED:
			return 'X';

		default:
			return '?';
	}
}

API_LIBLOCAL int transaction_log_write_phase(transaction_log_t *transaction_log, transaction_id_t transaction_id, transaction_phase_e phase) {
	char phase_encoded;

	assert(transaction_log != NULL);
	assert(phase != UNKNOWN);
	
	phase_encoded = transaction_log_phase_to_char(phase);

	io_seek(transaction_log->file, transaction_log_get_offset(transaction_log, transaction_id), SEEK_SET);
	return (io_write(&phase_encoded, 1, 1, transaction_log->file) == 1 ? 0 : -1);
}

API_LIBLOCAL int transaction_log_read_phase(transaction_log_t *transaction_log, transaction_id_t transaction_id, transaction_phase_e *phase) {
	char phase_encoded;

	assert(transaction_log != NULL);
	assert(phase != NULL);

	io_seek(transaction_log->file, transaction_log_get_offset(transaction_log, transaction_id), SEEK_SET);
	if (io_read(&phase_encoded, 1, 1, transaction_log->file) != 1)
		return -1;

	*phase = transaction_log_char_to_phase(phase_encoded);

	return 0;
}

API_LIBLOCAL transaction_phase_e *transaction_log_dump(transaction_log_t *transaction_log, transaction_id_t *first_id, transaction_id_t *last_id) {
	int i;
	char *buffer;
	transaction_phase_e *dump;
	ssize_t rv;

	assert(transaction_log != NULL);
	assert(first_id != NULL);
	assert(last_id != NULL);

	/* Read lock on current transaction number. */
	transaction_log_readlock_current(transaction_log);

	/* Retrieve the current transaction ID. */
	transaction_log_get_current(transaction_log, last_id);

	/* Temporarily, assume full log. */
	buffer = malloc(sizeof(*buffer) * transaction_log->maxitems);

	io_seek(transaction_log->file, transaction_log_get_offset(transaction_log, 0), SEEK_SET);
	transaction_log_flush(transaction_log);
	rv = io_read(buffer, sizeof(*buffer), transaction_log->maxitems, transaction_log->file);
	if (rv < 0)
		return NULL;

	dump = malloc(sizeof(*dump) * rv);
	for (i = 0; i < rv; i++) {
		dump[i] = transaction_log_char_to_phase(buffer[i]);
	}
	*first_id = *last_id - (transaction_id_t)rv;
	free(buffer);

	assert((size_t)rv == transaction_log->maxitems);

	/* Release the read lock. */
	transaction_log_unlock_current(transaction_log);

	return dump;
}

API_LIBLOCAL int transaction_log_lock_item(transaction_log_t *transaction_log, transaction_id_t transaction_id) {
	assert(transaction_log != NULL);

	return filelock_writelock(transaction_log->file, transaction_log_get_offset(transaction_log, transaction_id), 1);
}

API_LIBLOCAL int transaction_log_unlock_item(transaction_log_t *transaction_log, transaction_id_t transaction_id) {
	assert(transaction_log != NULL);

	return filelock_unlock(transaction_log->file, transaction_log_get_offset(transaction_log, transaction_id), 1);
}

API_LIBLOCAL int transaction_log_checklock(transaction_log_t *transaction_log, transaction_id_t transaction_id) {
	assert(transaction_log != NULL);

	return filelock_checklock(transaction_log->file, transaction_log_get_offset(transaction_log, transaction_id), 1);
}

API_LIBLOCAL int transaction_log_writelock_current(transaction_log_t *transaction_log) {
	assert(transaction_log != NULL);

	return filelock_writelock(transaction_log->file, 0, sizeof(transaction_id_t));
}

API_LIBLOCAL int transaction_log_readlock_current(transaction_log_t *transaction_log) {
	assert(transaction_log != NULL);

	return filelock_readlock(transaction_log->file, 0, sizeof(transaction_id_t));
}

API_LIBLOCAL int transaction_log_unlock_current(transaction_log_t *transaction_log) {
	assert(transaction_log != NULL);

	return filelock_unlock(transaction_log->file, 0, sizeof(transaction_id_t));
}

API_LIBLOCAL int transaction_log_writelock_vacuum(transaction_log_t *transaction_log) {
	assert(transaction_log != NULL);

	return filelock_writelock(transaction_log->file, sizeof(transaction_id_t), sizeof(transaction_id_t));
}

API_LIBLOCAL int transaction_log_readlock_vacuum(transaction_log_t *transaction_log) {
	assert(transaction_log != NULL);

	return filelock_readlock(transaction_log->file, sizeof(transaction_id_t), sizeof(transaction_id_t));
}

API_LIBLOCAL int transaction_log_unlock_vacuum(transaction_log_t *transaction_log) {
	assert(transaction_log != NULL);

	return filelock_unlock(transaction_log->file, sizeof(transaction_id_t), sizeof(transaction_id_t));
}
